Skip to content

new: form and a11y package for Solid 2.0#926

Open
davedbase wants to merge 25 commits into
solidjs-community:nextfrom
davedbase:v2/form
Open

new: form and a11y package for Solid 2.0#926
davedbase wants to merge 25 commits into
solidjs-community:nextfrom
davedbase:v2/form

Conversation

@davedbase

@davedbase davedbase commented Jun 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Introduces createForm<C> and toFormData for Solid 2.0
  • Per-field signals (value, error, touched, pending), form-level signals (dirty, valid, submitting, submitted), and DOM binding via the 2.0 two-phase ref directive pattern
  • Sync and async validators are first-class; validators are plain (value) => string | null | Promise<string | null> functions — no adapter needed for Zod, Valibot, Arktype, etc.
  • validateOn: "change" | "blur" | "submit" controls error display timing, per field or form-wide
  • Cross-field rules via form.validate(fn); server-side errors via form.setError(name, msg)
  • SSR-safe: static accessors on the server, no reactive primitives created

Implementation notes (fixes applied during review)

Bug fix — async validators fired 3× per keystroke
The original architecture called each validator from three code paths per value change: once from the _rawError memo, once from the effect's sync-check, and once from the effect's collection loop. For async validators making network requests, this meant 3 API calls per keystroke with only the last result used.

Fixed by pre-classifying validators at setup time (one probe call each with fc.initial): sync validators go into a lazy syncMemo that is the only reactive subscriber to value() and never touches async validators; async validators go into asyncFns. The initial async Promises from classification are saved and reused on the first effect run if the value hasn't changed, avoiding a second API call. When async settles and asyncError() changes, _rawError recomputes by reading signals only — no validator is called. Result: async validators fire once per value change.

toFormDatafalse now omitted
Previously false was coerced to the string "false", which doesn't match HTML's native behavior (unchecked checkboxes are absent from form payloads). false is now omitted alongside null and undefined.

reset(newValues?) overload
Accepts an optional partial values object. Named fields adopt the new values as their dirty baseline, making edit-form patterns possible without fighting the initial-value comparison. A version counter forces dirty to recompute even when the field value and new baseline are identical (no-op signal write wouldn't invalidate the memo otherwise).

setError / field.setError
Injects an external (server-side) error into the reactive graph. Appears in field.error(), form.errors(), and gates form.valid(). Cleared automatically when the user edits the field (setValue calls the wrapped setter which clears it).

Documentation fixes

  • README type definition for bind was missing | HTMLTextAreaElement
  • validate() note incorrectly said "before form.valid() is first read" — the version counter handles late registration
  • errors() table row now documents the exclusion of cross-field validate() errors

Summary by CodeRabbit

  • New Features

    • Added a reactive form library with field-level validation, error gating, cross-field rules, async validation, DOM binding, and form submission handling.
    • Added accessibility primitives for form controls including ARIA label/description/error chaining and validation state propagation.
    • Added live region announcements for screen reader messaging.
    • Added utilities for reduced motion preferences detection.
  • Documentation

    • Added comprehensive README and Storybook stories demonstrating form validation patterns, cross-field rules, async validators, and ARIA form field integration.
  • Tests

    • Added full test coverage for form and accessibility functionality including SSR compatibility.

@davedbase davedbase added this to the Solid 2.0 Migration milestone Jun 3, 2026
@changeset-bot

changeset-bot Bot commented Jun 3, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: c395f96

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d38c2cf-ece7-4872-85d2-09898c14e697

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces two new Solid.js primitive packages: @solid-primitives/a11y providing ARIA accessibility utilities (live-region announcements, reduced-motion detection, form control ARIA wiring) and @solid-primitives/form providing reactive form state management with validation, DOM binding, and submission handling. Both packages include comprehensive TypeScript types, documentation, Storybook stories, and test suites covering SSR behavior.

Changes

A11Y Accessibility Primitives

Layer / File(s) Summary
A11Y package configuration and types
packages/a11y/LICENSE, packages/a11y/package.json, packages/a11y/tsconfig.json, packages/a11y/README.md, packages/a11y/src/types.ts, packages/a11y/src/index.ts
Package metadata, TypeScript project configuration, and public type definitions for CreateFormControlProps, CreateFormControlInputProps, FormControlDataSet, and FormControlContextValue are added. Full API documentation in README covers all exported primitives.
Announcement utilities and reduced motion
packages/a11y/src/announce.ts, packages/a11y/src/reduced-motion.ts, packages/a11y/test/index.test.tsx, packages/a11y/test/server.test.ts
createAnnounce/makeAnnounce implement ARIA live-region messaging with polite and assertive modes and 50ms debouncing. createReducedMotion returns a reactive Accessor<boolean> for the prefers-reduced-motion media query with SSR safety. Tests cover DOM management, timer behavior, listener cleanup, and server-side no-ops.
Form control ARIA wiring and context
packages/a11y/src/form-control.ts
FormControlContext, useFormControl(), createFormControl(), and createFormControlInput() implement reactive ARIA wiring for labeled form controls. Supports ID generation, aria-labelledby/aria-describedby computation from registered label/description/error elements, dataset-driven state attributes, and re-registration on field ID changes. Tests verify context provider/consumer relationships, ARIA chain computation, ID cleanup, and reactive updates.
A11Y stories, tests, and SSR support
packages/a11y/stories/a11y.stories.tsx
Storybook stories demonstrate createAnnounce with polite/assertive toggling, createReducedMotion with OS preference mirroring and override, and createFormControl usage both standalone and via context provider patterns. Sub-component and context provider patterns show label/input/description/error registration with TextFieldRoot. Validation state propagation and aria-labelledby chaining are documented with interactive examples. SSR tests confirm graceful server-side degradation.

Form Primitive Package

Layer / File(s) Summary
Form package configuration and public contract
packages/form/LICENSE, packages/form/package.json, packages/form/tsconfig.json, packages/form/README.md, packages/form/src/types.ts, packages/form/src/index.ts
Package metadata, build configuration, and TypeScript type definitions define the form API contract: ValidatorFn (sync/async), FieldConfig, FormField, FormReturn, FormConfig, with generic value inference. Types are re-exported from both local and @solid-primitives/a11y modules. Comprehensive README documents all APIs, configuration options, and validation patterns.
Form runtime, field state, and validation
packages/form/src/form.ts (core setup and fields), packages/form/test/index.test.ts, packages/form/test/server.test.ts
createForm() implements reactive per-field state (value, touched, error, pending) and form-level derived signals (values, errors, dirty, valid, pending, submitting, submitted). Validators are classified as sync or async by probing the initial value; sync errors are memoized, async errors computed in effects with stale result discarding. Errors are gated by validateOn (change/blur/submit) and touched/submitted state. Tests cover field initialization, sync validation, error reactivity, reset behavior, and SSR field state returns.
Async validators, cross-field rules, and bulk operations
packages/form/src/form.ts (advanced state, helpers), packages/form/test/index.test.ts
Async validators run after sync validation passes, set pending() during execution, and discard stale results when field values change. Cross-field rules via form.validate(fn) return reactive error accessors that trigger valid() recomputation. Bulk operations (setValues, setError, reset) and toFormData() conversion (omitting null/undefined/false) provide form-level mutations. External errors clear on setValue/reset and have lower precedence than validator errors. Tests verify validateOn gating, async invocation counts, stale result handling, reactive invalidation, and error aggregation.
Form submission, DOM binding, and interactive examples
packages/form/src/form.ts (submission & binding), packages/form/stories/form.stories.tsx, packages/form/test/index.test.ts, packages/form/test/server.test.ts
Form submission via form.submit() touches all fields, gates on valid(), calls onSubmit with untracked values, and guards against concurrent submissions. form.bind(name) syncs inputs/checkboxes/radios/textarea/select bi-directionally: initializes from field state, updates field on input/change, sets touched on blur, and cleans up listeners. form.ref(formEl) intercepts native submit events. Storybook stories show validateOn: "change" for immediate feedback, "blur" for lazy validation, "submit" for clean form state while typing, cross-field confirm matching, and async availability checking with pending indicators. SSR tests verify no-op bindings and safe submit()/reset().
Storybook UI text field components for form control
.storybook/ui/form-control.tsx, .storybook/ui/index.ts
TextFieldRoot, TextFieldLabel, TextFieldInput, TextFieldDescription, and TextFieldErrorMessage components provide a complete form-control implementation. TextFieldRoot creates and provides the form-control context with data-* dataset attributes for styling. TextFieldInput derives ARIA attributes from context and applies validation-state-dependent borders/shadows. TextFieldDescription and TextFieldErrorMessage register IDs and conditionally mount based on validation state. UI entrypoint exports all components for use in Storybook stories.
Form control backward compatibility re-export
packages/form/src/form-control.ts, package.json
packages/form/src/form-control.ts is converted to a re-export module forwarding createFormControl, createFormControlInput, FormControlContext, and useFormControl from @solid-primitives/a11y. Root package.json adds @solid-primitives/a11y as a dev dependency. Documentation comments explain the move for backward compatibility.

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • atk
  • brenelz

🐰 Two primitives now dance in harmony fine,
Forms and a11y, a story divine!
Fields that validate, ARIA that sings,
SSR-safe, with accessible rings!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: introducing new form and a11y packages for Solid 2.0.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@davedbase davedbase marked this pull request as ready for review June 3, 2026 12:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/form/README.md`:
- Around line 100-115: The README's public API table and method list omit the
public method setValues, which is declared on FormReturn and implemented in
form.ts; add an entry for setValues to the API table and the definition block
describing its signature and behavior (e.g., setValues(newValues?:
Partial<Values>) => void, updates current values and optionally resets
baselines), referencing the same terminology as FormReturn and the
implementation in form.ts so documentation and code remain consistent.

In `@packages/form/src/form.ts`:
- Line 100: The SSR branch currently returns a blank FormData via formData: ()
=> new FormData(), causing mismatch with client behavior which uses
toFormData(values()); change the SSR stub so formData() returns the serialized
initial values by calling toFormData(values()) (ensure toFormData is
imported/available and values() is referenced the same way as in the client
branch) so server consumers receive the same payload as the hydrated client.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e75dd6f3-96a8-4c3e-bca6-67e50d15352b

📥 Commits

Reviewing files that changed from the base of the PR and between fd4b2c2 and 9122fca.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • packages/form/README.md
  • packages/form/package.json
  • packages/form/src/form.ts
  • packages/form/src/index.ts
  • packages/form/src/types.ts
  • packages/form/stories/form.stories.tsx
  • packages/form/test/index.test.ts
  • packages/form/test/server.test.ts
  • packages/form/tsconfig.json

Comment thread packages/form/README.md
Comment thread packages/form/src/form.ts Outdated
@davedbase davedbase changed the title new: form package for Solid 2.0 new: form and a11y package for Solid 2.0 Jun 13, 2026
@davedbase

Copy link
Copy Markdown
Member Author

@coderabbitai can you please re-review this package

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

@davedbase Sure! I'll kick off a full re-review of this PR right away.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/form/README.md (1)

17-23: ⚡ Quick win

Specify language for fenced code block.

The installation commands should specify bash or shell as the language for proper syntax highlighting and better accessibility.

📝 Suggested fix
-```
+```bash
 npm install `@solid-primitives/form`
 # or
 yarn add `@solid-primitives/form`
 # or
 pnpm add `@solid-primitives/form`
 ```
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/form/README.md` around lines 17 - 23, Update the fenced code block
that contains "npm install `@solid-primitives/form`" so it specifies a shell
language for syntax highlighting (e.g., add ```bash or ```shell at the opening
fence); keep the three install lines unchanged and only modify the opening fence
to include the language token for proper highlighting and accessibility.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/a11y/README.md`:
- Around line 15-21: Two unlabeled fenced code blocks (the install commands
block beginning with "npm install `@solid-primitives/a11y`" and the example block
starting "// No label registered, no aria-label") need explicit language
identifiers to satisfy markdownlint MD040; update the first fence to ```bash and
the second to ```txt so the snippets become fenced with those language tags.
Ensure only the opening backticks are modified (leave the content and closing
``` untouched) so the install snippet uses bash and the comment/example snippet
uses txt.

---

Nitpick comments:
In `@packages/form/README.md`:
- Around line 17-23: Update the fenced code block that contains "npm install
`@solid-primitives/form`" so it specifies a shell language for syntax highlighting
(e.g., add ```bash or ```shell at the opening fence); keep the three install
lines unchanged and only modify the opening fence to include the language token
for proper highlighting and accessibility.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 31ace794-014e-46bf-ae00-787f13b6fd63

📥 Commits

Reviewing files that changed from the base of the PR and between 66b539b and 755867d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (26)
  • .storybook/ui/form-control.tsx
  • .storybook/ui/index.ts
  • package.json
  • packages/a11y/LICENSE
  • packages/a11y/README.md
  • packages/a11y/package.json
  • packages/a11y/src/announce.ts
  • packages/a11y/src/form-control.ts
  • packages/a11y/src/index.ts
  • packages/a11y/src/reduced-motion.ts
  • packages/a11y/src/types.ts
  • packages/a11y/stories/a11y.stories.tsx
  • packages/a11y/test/index.test.tsx
  • packages/a11y/test/server.test.ts
  • packages/a11y/tsconfig.json
  • packages/form/LICENSE
  • packages/form/README.md
  • packages/form/package.json
  • packages/form/src/form-control.ts
  • packages/form/src/form.ts
  • packages/form/src/index.ts
  • packages/form/src/types.ts
  • packages/form/stories/form.stories.tsx
  • packages/form/test/index.test.ts
  • packages/form/test/server.test.ts
  • packages/form/tsconfig.json

Comment thread packages/a11y/README.md Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants